# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the MIT license.
# See the LICENSE file in the project root for more information.

from __future__ import annotations  # Allow Field[Any]
from dataclasses import fields, Field, is_dataclass
from pathlib import Path
from textwrap import indent
from typing import Any, Optional, Set, Type

from .bench_file import (
    AllocType,
    BenchFile,
    Benchmark,
    BenchOptions,
    CollectKind,
    Config,
    CoreclrSpecifier,
    GCPerfSimArgs,
)
from .config import DOCS_PATH
from .option import optional_to_iter
from .parse_and_serialize import to_yaml
from .type_utils import get_field_info, iter_classes_in_type, match_type, todo, unindent_doc
from .util import update_file

_BENCHFILE_MD_PATH: Path = DOCS_PATH / "bench_file.md"

_EXAMPLE_BENCHFILE = BenchFile(
    options=BenchOptions(collect=CollectKind.gc, default_iteration_count=3),
    coreclrs={
        "clr_a": CoreclrSpecifier(
            core_root=Path("./coreclr"), commit_hash="930abba4060fb528db2bb9835a1bc5a6e684bfec"
        ),
        "clr_b": CoreclrSpecifier(
            core_root=Path("./coreclr2"), commit_hash="ed52a006c01a582d4d34add40c318d6f324b99ba"
        ),
    },
    common_config=Config(complus_gcserver=True, complus_gcconcurrent=False),
    configs={
        "smaller": Config(complus_gcgen0size=0x1000000),
        "bigger": Config(complus_gcgen0size=0x2000000),
    },
    benchmarks={
        "nosurvive": Benchmark(
            executable="GCPerfSim",
            arguments=GCPerfSimArgs(
                tc=8,
                lohar=0,
                tagb=500,
                tlgb=1,
                sohsi=50,
                lohsi=0,
                sohpi=0,
                lohpi=0,
                allocType=AllocType.reference,
            ),
        )
    },
)

_INITIAL_TEXT = f"""
(This file is generated by `py . lint`)

A benchfile lets us vary three different things: coreclrs, configs, and benchmarks.
The test runner uses all combinations of coreclrs ⨯ configs ⨯ benchmarks.
Each is a map with keys being arbitrary names.

Here's an example benchfile:

```yaml
{to_yaml(_EXAMPLE_BENCHFILE)}
```


# Detailed documentation of each type


""".lstrip()


def update_benchfile_md() -> None:
    text = _INITIAL_TEXT + _document_class_and_all_referenced_classes(BenchFile)
    update_file(_BENCHFILE_MD_PATH, text)


def _document_class_and_all_referenced_classes(cls: Type[Any]) -> str:
    assert is_dataclass(cls)
    all_classes_set: Set[Type[Any]] = set()
    _collect_all_classes_to_document(cls, all_classes_set)
    all_classes = sorted(all_classes_set, key=lambda cls: cls.__name__)
    return "\n\n\n".join(_describe_class(cls) for cls in all_classes)


def _indent_doc(doc: str) -> str:
    return indent(unindent_doc(doc), "  ")


def _describe_class(cls: Type[Any]) -> str:
    # @dataclass will automatically add documentation like "ClassName(x: int, y: int)".
    # We only want to display manually-written docs
    doc = cls.__doc__
    assert doc is not None
    is_automatic_doc = doc.startswith(cls.__name__ + "(")
    doc_str = "" if is_automatic_doc else _indent_doc(doc) + "\n"
    fields_str = "\n".join(
        d for f in fields(cls) for d in optional_to_iter(_describe_field(cls, f))
    )
    return f"## {cls.__name__}\n{doc_str}\n{fields_str}"


def _describe_field(cls: Type[Any], fld: Field[Any]) -> Optional[str]:
    head = f"{fld.name}: `{_show_type(fld.type)}`"
    info = get_field_info(cls, fld)
    if info.hidden:
        return None
    elif info.doc is None:
        return head
    else:
        return head + "\n" + indent(unindent_doc(info.doc), "  ") + "\n"


def _collect_all_classes_to_document(cls: Type[Any], out: Set[Type[Any]]) -> None:
    assert is_dataclass(cls)
    if cls not in out:
        out.add(cls)
        for fld in fields(cls):
            if not get_field_info(cls, fld).hidden:
                field_type = fld.type
                for t in iter_classes_in_type(field_type):
                    if is_dataclass(t):
                        _collect_all_classes_to_document(t, out)


def _show_type(t: Type[Any]) -> str:
    return match_type(
        t,
        default_handler=lambda cls: todo(str(cls)),
        handle_primitive=lambda p: "None" if p is type(None) else p.__name__,
        handle_union=lambda union: " | ".join(_show_type(u) for u in union),
        handle_enum=lambda enum_members: " | ".join(f'"{m}"' for m in enum_members),
        handle_sequence=lambda seq_element_type: f"Sequence[{_show_type(seq_element_type)}]",
        handle_tuple=lambda tuple_elements: (
            f"Tuple[{', '.join(_show_type(e) for e in tuple_elements)}]"
        ),
        handle_mapping=lambda k, v: f"Mapping[{_show_type(k)}, {_show_type(v)}]",
        # We create a MarkDown header for each class, so we can link to it here.
        handle_dataclass=lambda cls: f"[{cls.__name__}](#{cls.__name__})",
    )
